防抖和节流

防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于 wait,防抖的情况下只会调用一次,而节流的情况会每隔一定时间(参数 wait)调用函数。

防抖

function now() {
  return +new Date();
}
/**
 * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
 *
 * @param  {function} func        回调函数
 * @param  {number}   wait        表示时间窗口的间隔
 * @param  {boolean}  immediate   设置为 true 时,是否立即调用函数
 * @return {function}             返回客户调用函数
 */
function debounce(func, wait = 50, immediate = true) {
  let timer, context, args;

  // 延迟执行函数
  const later = () =>
    setTimeout(() => {
      // 延迟函数执行完毕,清空缓存的定时器序号
      timer = null;
      // 延迟执行的情况下,函数会在延迟函数中执行
      // 使用到之前缓存的参数和上下文
      if (!immediate) {
        func.apply(context, args);
        context = args = null;
      }
    }, wait);

  // 这里返回的函数是每次实际调用的函数
  return function (...params) {
    // 如果没有创建延迟执行函数(later),就创建一个
    if (!timer) {
      timer = later();
      // 如果是立即执行,调用函数
      // 否则缓存参数和调用上下文
      if (immediate) {
        func.apply(this, params);
      } else {
        context = this;
        args = params;
      }
      // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
      // 这样做延迟函数会重新计时
    } else {
      clearTimeout(timer);
      timer = later();
    }
  };
}

函数防抖就是在函数需要频繁触发的情况下,只有足够的空闲时间,才执行一次。

function debounce(handler, delay) {
  delay = delay || 300;
  var timer = null;

  return function () {
    var _self = this,
      _args = arguments;

    clearTimeout(timer);
    timer = setTimeout(function () {
      handler.apply(_self, _args);
    }, delay);
  };
}

防抖:高频事件触发 n 秒后才执行,如果 n 秒内重新触发,则重新计算时间,形象类比于坐电梯,实际可运用在输入框实时搜索

var btn = document.getElementById("button");
function _debounce(fn, wait) {
  var timer = null;
  return function () {
    clearTimeout(timer);
    console.log("1123");
    timer = setTimeout(() => {
      fn();
    }, wait);
  };
}

btn.onclick = _debounce(_log, 5000);
function _log() {
  console.log(1);
}

三个参数

function debounce(func, wait, immediate) {
  let timeout, result;
  const debounced = function () {
    const context = this;
    const args = arguments;
    if (timeout) clearTimeout(timeout);
    if (immediate) {
      // 如果已经执行过,不再执行
      const callNow = !timeout;
      timeout = setTimeout(function () {
        timeout = null;
      }, wait);
      if (callNow) result = func.apply(context, args);
    } else {
      timeout = setTimeout(function () {
        func.apply(context, args);
      }, wait);
    }
    return result;
  };
  debounced.cancel = function () {
    clearTimeout(timeout);
    timeout = null;
  };
  return debounced;
}

节流

滚动事件中会发起网络请求,但是我们并不希望用户在滚动过程中一直发起请求,而是隔一段时间发起一次,对于这种情况我们就可以使用节流。

// func是用户传入需要防抖的函数
// wait是等待时间
const throttle = (func, wait = 50) => {
  // 上一次执行该函数的时间
  let lastTime = 0;
  return function (...args) {
    // 当前时间
    let now = +new Date();
    // 将当前时间和上一次执行函数时间对比
    // 如果差值大于设置的等待时间就执行函数
    if (now - lastTime > wait) {
      lastTime = now;
      func.apply(this, args);
    }
  };
};

setInterval(
  throttle(() => {
    console.log(1);
  }, 500),
  1
);

函数节流(throttle)是指阻止一个函数在很短时间间隔内连续调用。只有当上一次函数执行后达到规定的时间间隔,才能进行下一次调用。但要保证一个累计最小调用间隔(否则拖拽类的节流都将无连续效果)

防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

/**
 * underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
 *
 * @param  {function}   func      回调函数
 * @param  {number}     wait      表示时间窗口的间隔
 * @param  {object}     options   如果想忽略开始函数的的调用,传入{leading: false}。
 *                                如果想忽略结尾函数的调用,传入{trailing: false}
 *                                两者不能共存,否则函数不能执行
 * @return {function}             返回客户调用函数
 */
_.throttle = function (func, wait, options) {
  var context, args, result;
  var timeout = null;
  // 之前的时间戳
  var previous = 0;
  // 如果 options 没传则设为空对象
  if (!options) options = {};
  // 定时器回调函数
  var later = function () {
    // 如果设置了 leading,就将 previous 设为 0
    // 用于下面函数的第一个 if 判断
    previous = options.leading === false ? 0 : _.now();
    // 置空一是为了防止内存泄漏,二是为了下面的定时器判断
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };
  return function () {
    // 获得当前时间戳
    var now = _.now();
    // 首次进入前者肯定为 true
    // 如果需要第一次不执行函数
    // 就将上次时间戳设为当前的
    // 这样在接下来计算 remaining 的值时会大于0
    if (!previous && options.leading === false) previous = now;
    // 计算剩余时间
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    // 如果当前调用已经大于上次调用时间 + wait
    // 或者用户手动调了时间
    // 如果设置了 trailing,只会进入这个条件
    // 如果没有设置 leading,那么第一次会进入这个条件
    // 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
    // 其实还是会进入的,因为定时器的延时
    // 并不是准确的时间,很可能你设置了2秒
    // 但是他需要2.2秒才触发,这时候就会进入这个条件
    if (remaining <= 0 || remaining > wait) {
      // 如果存在定时器就清理掉否则会调用二次回调
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
      // 判断是否设置了定时器和 trailing
      // 没有的话就开启一个定时器
      // 并且不能不能同时设置 leading 和 trailing
      timeout = setTimeout(later, remaining);
    }
    return result;
  };
};

一个函数只有在大于执行周期时才执行,周期内调用不执行。好像水滴积攒到一定程度才会触发一次下落一样。

典型应用:

  • 抢券时疯狂点击,既要限制次数,又要保证先点先发出请求
  • 窗口调整
  • 页面滚动
function throttle(handler, wait) {
  wait = wait || 300;
  var lastTime = 0;

  return function () {
    var _self = this,
      _args = arguments;

    var nowTime = new Date().getTime();
    if (nowTime - lastTime > wait) {
      handler.apply(_self, _args);
      lastTime = nowTime;
    }
  };
}

三个参数

function throttle(func, wait, options) {
  let timeout, context, args, result;
  let previous = 0;
  if (!options) options = {};
  console.log(options.leading === false);
  const later = function () {
    previous = options.leading === false ? 0 : new Date().getTime();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };
  const throttled = function () {
    const now = new Date().getTime();
    if (!previous && options.leading === false) previous = now;
    console.log(`now${now}`);
    console.log(`previous${previous}`);
    const remaining = wait - (now - previous);
    context = this;
    args = arguments;
    if (remaining <= 0 || remaining > wait) {
      console.log(2222);
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
      timeout = setTimeout(later, remaining);
    }
    return result;
  };
  throttled.cancel = function () {
    clearTimeout(timeout);
    previous = 0;
    timeout = null;
  };
  return throttled;
}

手写 (常考点)

function debounce(fn, delay) {
  delay = delay || 500;
  let timer;
  return function () {
    let args = arguments;
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null;
    }, delay);
  };
}
function throttle(fn, delay) {
  delay = delay || 500;
  let timer;
  return function () {
    let args = arguments;
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, delay);
    }
  };
}
/**
 * 节流:动作绑定事件,动作发生后一段时间后触发事件,
 * 在这段时间内,如果动作有发生了,则无视该动作,直到时间执行完后,才能重新触发
 * 原理:在每次函数执行前先判断是否存在定时器,存在则跳过本次执行,否则设置定时器
 */
function throttle(fn, wait) {
  var pre = Date().now();
  var self = this;
  var context = arguments;
  return function () {
    var now = Date.now();
    if (now - prev >= wait) {
      fun.apply(self, context);
      pre = Date.now();
    }
  };
}

/**
 * 防抖:
 * 动作发生后在一定时间内触发事件,
 * 在这段时间内,如果动作发生了,则重新等待一定时间在触发事件
 *
 * 原理:在每次函数执行前先清空上一次设置的定时器
 * */
function debounce(fn, wait) {
  var timer;
  var self = this;
  var context = arguments;
  return function () {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(self, context);
    }, wait);
  };
}
Last Updated:
Contributors: yiliang114